#pragma once

#include "lims/entities/Reference.hpp"
#include "lims/entities/Point.hpp"
#include "lims/model/PolynomialRegression.h"
#include "lims/common/Constants.h"

#include <climits>
#include <cmath>
#include <algorithm>

using namespace std;

namespace MetricSpaceBenchmark::MLIndex::LIMS {
    //////////////////////////////////////////枢轴点选取算法: FFT//////////////////////////////////////////////////////////////
    // caculate reference of each cluster
    class ChooseRef {
    private:
        // number of reference point
        unsigned num_ref{};   // 簇内枢轴点数量
        // dimentional of data point
        unsigned dim{};
        // total number of data set
        int num_data{};  // 簇内数据点数量

        Point mainRefPoint;  // 簇心
        vector<Ref_Point> RefPoint_Set;//每个簇内枢轴点集合

        vector<double> mainRefDisArr;  // 簇心到簇内各个数据点距离
    public:
        mainRef_Point main_pointSet;
        vector<double> Pos;   // 数据存放的下标位置

        ChooseRef();

        ChooseRef(unsigned, Clu_Point &, Point &, vector<Point> &, int);

        vector<Ref_Point> ChooseRefPoint(Clu_Point &);

        vector<Ref_Point> ChooseRefPoint_Input(Clu_Point &, vector<Point> &);

        double CaculateEuclideanDis(Point &, Point &) const;

        vector<double> CaculateDisArr(Clu_Point &, Point &);

        mainRef_Point getMainRefPoint() const;

        static vector<double> TrainModel(vector<double> &, vector<double> &);
    };

    ChooseRef::ChooseRef() = default;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ChooseRef::ChooseRef(unsigned num_ref, Clu_Point &cluster, Point &main_pivot, vector<Point> &oth_pivot, int type) {
        // 每个簇内枢轴点数量
        this->num_ref = num_ref;
        this->dim = cluster.clu_point[0].coordinate.size();    // 数据维度数
        // 该簇的数据量
        this->num_data = cluster.clu_point.size();

        for (int i = 0; i < num_data; i++)
            Pos.push_back(i);

        this->mainRefPoint = main_pivot;
        // corner
        if (type == 0)
            this->RefPoint_Set = ChooseRefPoint(cluster);
        else
            // by input
            this->RefPoint_Set = ChooseRefPoint_Input(cluster, oth_pivot);

        this->mainRefDisArr = CaculateDisArr(cluster, mainRefPoint);

        // 簇心和枢轴点
        this->main_pointSet = mainRef_Point(mainRefPoint, mainRefDisArr[num_data - 1], mainRefDisArr[0], RefPoint_Set);

        this->main_pointSet.setMainRefDisArr(mainRefDisArr);

        vector<double> mainRefPt_coeffs = TrainModel(main_pointSet.dis, Pos);

        this->main_pointSet.setCoeffs(mainRefPt_coeffs);
    }

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    vector<Ref_Point> ChooseRef::ChooseRefPoint(Clu_Point &cluster) {
        vector<Ref_Point> ref_points;
        for (unsigned i = 0; i < num_ref; i++) {
            double max = cluster.clu_point[0].coordinate[i];
            Point assRef_point = cluster.clu_point[0];
            for (int j = 1; j < num_data; j++) {
                if (cluster.clu_point[j].coordinate[i] > max) {
                    max = cluster.clu_point[j].coordinate[i];
                    assRef_point = cluster.clu_point[j];
                }
            }

            vector<double> dis = CaculateDisArr(cluster, assRef_point);
            Ref_Point ref_point = Ref_Point(assRef_point, dis[num_data - 1], dis[0]);

            ref_point.setDisArr(dis);

            vector<double> coeffs = TrainModel(ref_point.dis, Pos);
            ref_point.setCoeffs(coeffs);

            ref_points.push_back(ref_point);
        }
        return ref_points;
    }

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    vector<Ref_Point> ChooseRef::ChooseRefPoint_Input(Clu_Point &cluster, vector<Point> &oth_pivot) {
        vector<Ref_Point> ref_points;
        for (unsigned i = 0; i < num_ref; i++) {

            Point assRef_point = oth_pivot[i];   // 循环选择输入的参考点

            // 计算出该枢轴点到簇内其余各点间的距离
            vector<double> dis = CaculateDisArr(cluster, assRef_point);

            // 初始化枢轴点及其距离的上下限
            Ref_Point ref_point = Ref_Point(assRef_point, dis[num_data - 1], dis[0]);

            // 设置枢轴点的距离排序数组
            ref_point.setDisArr(dis);

            // 训练好枢轴点模型系数
            vector<double> coeffs = TrainModel(ref_point.dis, Pos);
            ref_point.setCoeffs(coeffs);

            // 加入到结果集中
            ref_points.push_back(ref_point);
        }
        return ref_points;
    }

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    vector<double> ChooseRef::CaculateDisArr(Clu_Point &cluster, Point &refPoint) {
        vector<double> dis;
        for (int i = 0; i < num_data; i++) {
            double distance = CaculateEuclideanDis(refPoint, cluster.clu_point[i]);
            // Keep two decimal places
            // double a = (int)(distance * 100);
            // distance = a / 100;

            dis.push_back(distance);
        }

        /* sort distance vector
         * PlanA we get 1-d value by rank of distance list so we need to sort distance list
         * PlanB we get 1-d value by the point of which circle it is so there is no need for sort this array
         */
        // update : dist arr need to be sorted cuz we get rank of circle by (pos of arr)/(arr.len / num_cir)
        sort(dis.begin(), dis.end());
        return dis;
    }

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    double ChooseRef::CaculateEuclideanDis(Point &point_a, Point &point_b) const {
        double total = 0.0;
        for (unsigned i = 0; i < dim; i++) {
            total += pow(point_a.coordinate[i] - point_b.coordinate[i], 2);
        }
        return sqrt(total);
    }

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    mainRef_Point ChooseRef::getMainRefPoint() const {
        return main_pointSet;
    }

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    vector<double> ChooseRef::TrainModel(vector<double> &X, vector<double> &Y) {
        PolynomialRegression<double> polyreg;
        vector<double> coeffs;
        polyreg.fitIt(X, Y, Constants::COEFFS, coeffs);
        return coeffs;
    }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
